SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
10.2 KB · 199 lines tsx
Raw Blame History
1import { GitCompareArrows } from 'lucide-react';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { Suspense } from 'react';5import { Bars } from '@/components/charts/bars';6import { LineChart } from '@/components/charts/line-chart';7import { WorldMap } from '@/components/charts/world-map';8import { CompanyHeader, DensityStrip } from '@/components/company/company-header';9import { CompanyMiniList } from '@/components/company/company-table';10import { MetricTiles } from '@/components/company/metric-tiles';11import { ConfidenceLegend, HistoryPanel, JobsPanel, LocationsPanel, PeoplePanel, PricingPanel, ProductsPanel, SensorsTable, SignalsPanel, TimelinePanel } from '@/components/company/panels';12import { CorporateStructurePanel, KeyFactsPanel } from '@/components/company/profile-panels';13import { EventList } from '@/components/events/event-row';14import { Chip } from '@/components/ui/badges';15import { KV, Row } from '@/components/ui/key-value';16import { Container, Empty, Note } from '@/components/ui/section';17import { TabPanel, Tabs } from '@/components/ui/tabs';18import { api, ApiError, safe } from '@/lib/api';19import { fmtDate, fmtPct, humanize } from '@/lib/format';20import { bool, str, type SP } from '@/lib/params';21import { sourceFor } from '@/lib/profile';22import { routes, SURFACE_LABELS } from '@/lib/site';23import type { CompanyDetail, MapBucket } from '@/lib/types';2425export const revalidate = 120;2627const TABS = [28  { id: 'overview', label: 'Overview' },29  { id: 'timeline', label: 'Timeline' },30  { id: 'signals', label: 'Signals' },31  { id: 'jobs', label: 'Jobs' },32  { id: 'products', label: 'Products' },33  { id: 'pricing', label: 'Pricing' },34  { id: 'locations', label: 'Locations' },35  { id: 'leadership', label: 'Leadership' },36  { id: 'sources', label: 'Sources' },37  { id: 'history', label: 'History' },38];3940export default async function CompanyPage({ params, searchParams }: { params: Promise<{ slug: string }>; searchParams: Promise<SP> }) {41  const { slug } = await params;42  const sp = await searchParams;43  let c: CompanyDetail;44  try {45    c = await api.company(slug);46  } catch (e) {47    if (e instanceof ApiError && e.notFound) notFound();48    throw e;49  }50  const requested = str(sp.tab) ?? 'overview';51  const tab = TABS.some((t) => t.id === requested) ? requested : 'overview';52  const tabs = TABS.map((t) => ({ ...t, count: t.id === 'jobs' ? c.counts.jobs_open : t.id === 'sources' ? c.counts.sensors : t.id === 'signals' ? c.signals?.length || undefined : undefined }));53  // Products tab fallback: Wikidata "product or material produced" statements, with the provenance of that field.54  const wdProducts = c.profile?.products ?? [];55  const wdSrc = sourceFor(c.profile, 'products');56  const wdSource = wdSrc ? { url: wdSrc.url, retrieved_at: wdSrc.retrieved_at } : c.profile?.wikidata_url ? { url: c.profile.wikidata_url, retrieved_at: c.profile.enriched_at ?? '' } : null;5758  return (59    <Container wide>60      <CompanyHeader c={c} />61      <MetricTiles c={c} />62      <DensityStrip c={c} />63      <Suspense>64        <Tabs tabs={tabs} className="mt-4" sticky>65          <TabPanel id={tab}>66            {tab === 'overview' && <Overview c={c} />}67            {tab === 'timeline' && <TimelinePanel slug={c.slug} data={await safe(api.companyTimeline(c.slug, str(sp.filter) ?? 'all'))} filter={str(sp.filter) ?? 'all'} />}68            {tab === 'signals' && <SignalsPanel signals={c.signals ?? []} />}69            {tab === 'jobs' && <JobsPanel slug={c.slug} data={await safe(api.companyJobs(c.slug, { status: str(sp.status) ?? 'open', ai: bool(sp.ai) ? 1 : undefined, per_page: 50, page: str(sp.page) }))} status={str(sp.status) ?? 'open'} ai={bool(sp.ai)} />}70            {tab === 'products' && <ProductsPanel data={await safe(api.companyProducts(c.slug))} wikidataProducts={wdProducts} wikidataSource={wdSource} />}71            {tab === 'pricing' && <PricingPanel data={await safe(api.companyPricing(c.slug))} />}72            {tab === 'locations' && <Locations slug={c.slug} />}73            {tab === 'leadership' && <PeoplePanel data={await safe(api.companyPeople(c.slug))} />}74            {tab === 'sources' && <Sources c={c} />}75            {tab === 'history' && <HistoryPanel data={await safe(api.companyHistory(c.slug))} />}76          </TabPanel>77        </Tabs>78      </Suspense>79      <div className="mt-10 flex flex-wrap items-center justify-between gap-3 border-t border-rule pt-5">80        <p className="text-sm text-ink-2">Compare {c.display_name} with peers on activity, hiring, product velocity, AI adoption, locations and events.</p>81        <Link href={routes.compare([c.slug])} className="btn">82          <GitCompareArrows className="size-4" aria-hidden /> Compare companies83        </Link>84      </div>85    </Container>86  );87}8889async function Overview({ c }: { c: CompanyDetail }) {90  const [events, similar, metrics] = await Promise.all([safe(api.companyEvents(c.slug, { per_page: 10 })), safe(api.companySimilar(c.slug, 8)), safe(api.companyMetrics(c.slug, 90))]);91  const surfaces = Object.entries(c.sensors_by_surface ?? {}).sort((a, b) => b[1] - a[1]);92  const series = metrics?.series ?? {};93  const lines = (['activity_score', 'product_velocity', 'ai_adoption'] as const).filter((k) => (series[k]?.length ?? 0) > 1).map((k) => ({ id: k, label: humanize(k), points: series[k]!.map((p) => ({ day: p.day, value: p.value })) }));94  return (95    <div className="grid gap-8 lg:grid-cols-12">96      <div className="space-y-8 lg:col-span-8">97        <KeyFactsPanel c={c} />98        <section>99          <p className="eyebrow mb-2">Latest structured events</p>100          {events ? <EventList events={events.items} variant="table" showCompany={false} emptyLabel="No structured events detected for this company yet — sensors are attached and observing." /> : <Empty title="Events temporarily unavailable." />}101          {events && events.total > 10 && (102            <p className="mt-2 text-sm">103              <Link href={routes.company(c.slug, 'timeline')} className="link">104                Full timeline ({events.total} events) →105              </Link>106            </p>107          )}108        </section>109        <section>110          <p className="eyebrow mb-2">90-day metric series</p>111          {lines.length ? <LineChart series={lines} height={200} yZero /> : <Empty compact title="Not enough history for a series yet." />}112        </section>113        {c.signals?.length > 0 && (114          <section>115            <p className="eyebrow mb-2">Active signals</p>116            <SignalsPanel signals={c.signals.slice(0, 3)} />117          </section>118        )}119      </div>120      <aside className="space-y-8 lg:col-span-4">121        <section>122          <p className="eyebrow mb-2">Identity</p>123          <KV>124            <Row k="Legal name">{c.legal_name ?? '—'}</Row>125            <Row k="Domains">126              {c.domains?.length ? (127                <span className="mono text-xs">128                  {c.domains.map((d) => (129                    <span key={d.domain} className="block">130                      {d.domain} <span className="text-ink-3">· {d.kind}</span>131                    </span>132                  ))}133                </span>134              ) : (135                <span className="mono text-xs">{c.canonical_domain}</span>136              )}137            </Row>138            {c.aliases?.length > 0 && <Row k="Aliases">{c.aliases.join(', ')}</Row>}139            <Row k="Status">140              <Chip>{c.status.toLowerCase().replace(/_/g, ' ')}</Chip>141            </Row>142            <Row k="Importance">143              <span className="tnum">144                {c.importance} · tier {c.tier}145              </span>146            </Row>147            <Row k="Coverage">148              <span className="tnum">149                {fmtPct(c.coverage?.historical_coverage, 0)} · {c.coverage?.days_observed ?? '—'} days observed150                {c.coverage?.first_observed_at ? ` · since ${fmtDate(c.coverage.first_observed_at)}` : ''}151              </span>152            </Row>153            {c.coverage?.sensor_uptime !== null && c.coverage?.sensor_uptime !== undefined && (154              <Row k="Sensor uptime">155                <span className="tnum">{fmtPct(c.coverage.sensor_uptime, 1)}</span>156              </Row>157            )}158          </KV>159        </section>160        <CorporateStructurePanel relationships={c.relationships ?? []} />161        <section>162          <p className="eyebrow mb-2">Sensors by surface</p>163          {surfaces.length ? <Bars dense rows={surfaces.map(([s, n]) => ({ key: s, label: SURFACE_LABELS[s] ?? s, value: n }))} /> : <Empty compact />}164          <p className="mt-1 text-xs">165            <Link href={routes.company(c.slug, 'sources')} className="link">166              All sensors →167            </Link>168          </p>169        </section>170        <section>171          <p className="eyebrow mb-2">Similar companies</p>172          {similar ? <CompanyMiniList items={similar.items} metric="activity_score" label="activity" /> : <Empty compact />}173        </section>174        <ConfidenceLegend />175      </aside>176    </div>177  );178}179180async function Locations({ slug }: { slug: string }) {181  const data = await safe(api.companyLocations(slug));182  const buckets: MapBucket[] = (data?.items ?? []).filter((l) => l.status === 'listed' && l.lat !== null && l.lon !== null).map((l) => ({ lat: l.lat as number, lon: l.lon as number, country: l.country ?? '', city: l.city, companies: 1, events_30d: 1, jobs_open: 0, top: [] }));183  return <LocationsPanel data={data} map={buckets.length ? <WorldMap buckets={buckets} metric="companies" interactive={false} highlight={[...new Set(buckets.map((b) => b.country))]} /> : <Note>No geocoded locations to draw yet.</Note>} />;184}185186async function Sources({ c }: { c: CompanyDetail }) {187  const data = await safe(api.companySensors(c.slug));188  if (!data) return <Empty title="Sensors temporarily unavailable." />;189  const items = [...data.items].sort((a, b) => (a.status === b.status ? b.event_count - a.event_count : a.status === 'active' ? -1 : 1));190  return (191    <div className="space-y-3">192      <Note>193        Every fact on this profile traces back to one of these sensors. A sensor is a deployed connector watching one public URL on a schedule (tier A = 5–15 min … E = 3–7 days). Open a sensor for its observations, snapshots and changes.194      </Note>195      <SensorsTable items={items} />196    </div>197  );198}199